Skip to content

MiniMax-H3: flash attention in the video VAE, faster tile stitching, ggml-cuda speed patch - #17

Merged
oobabooga merged 5 commits into
masterfrom
perf/h3-gguf-speed
Oct 9, 2026
Merged

oobabooga merged 5 commits into
masterfrom
perf/h3-gguf-speed

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Problem

A MiniMax-H3 render (960x544, 124 frames) spends 30 to 140 s after sampling, mostly in the video VAE decode:

  • The VAE attention runs without flash attention. The H3 video VAE decoder is a 36-layer ViT, but --diffusion-fa does not reach it. It falls back to mul_mat + scale + softmax over 32xLxL f32 score matrices, about 6 s of GPU time per decode on a B200.
  • Tile stitching is cache-hostile. process_tiles_2d splits and merges tiles with the plane index in the inner loop, so every element is a cache and TLB miss across 105 tiles: about 10 s of single-threaded host time per decode.
  • ggml-cuda overheads in the quantised path. The "integrated device" flag is re-queried with cudaGetDeviceProperties about 2,800 times per decode, and row copies use a scalar kernel.

Changes

  • src/pipeline/diffusion_engine.cpp: on CUDA and ROCm, --diffusion-fa also enables flash attention in the H3 video VAE. The text encoder is untouched. On Vulkan the decode was slower with it (54.2 s against 53.2 s with it off on a Strix Halo), so other backends keep the previous attention unless SD_H3_VAE_FLASH_ATTN=1; SD_H3_VAE_FLASH_ATTN=0 turns it off everywhere. Documented in docs/minimax_h3.md.
  • src/runtime/tiling.cpp: the tile split and merge loops walk planes outermost. Output is bitwise identical.
  • scripts/unsloth/ggml-patches/0001-ggml-cuda-h3-speed.patch, applied by the prebuilt workflow after the ggml submodule checkout (the submodule points at a repository we do not push to; a patch that stops applying fails the build):
    • BF16 cuBLAS for quantised matmuls whose batch is at least GGML_CUDA_QUANT_CUBLAS_MIN_BATCH. Off by default, so default output is unchanged.
    • The device "integrated" flag is cached.
    • A vectorised row copy. Kill switch: GGML_CUDA_CPY_ROWS=0.

Results

MiniMax-H3 UD-Q3_K_XL, 960x544, 124 frames, 4 steps, same seed, each arm a fresh sd-cli on the same Colab VM, built from this branch with the patch applied.

card arm wall s decode s
RTX PRO 6000, offloaded master 99.6 31.6
RTX PRO 6000, offloaded this branch 75.9 17.1
RTX PRO 6000, resident master 80.1 27.2
RTX PRO 6000, resident this branch 63.0 12.8
RTX PRO 6000, resident this branch, BF16 path + --sage-attn 39.2 12.8
L4, offloaded master 506.4 136.7
L4, offloaded this branch 456.8 90.7
L4, offloaded this branch, BF16 path + --sage-attn 316.0 79.4

Accuracy

  • Bitwise identical to master: the tiling rewrite, the row copy, the props cache, and the BF16 path at its default (off).
  • VAE flash attention: 52.5 to 53.9 dB PSNR on video against master, audio identical. SD_H3_VAE_FLASH_ATTN=0 reproduces master exactly.
  • BF16 path (opt-in): 37.8 to 40.6 dB against master. Against an F32-dequant reference it is closer than the int8 path on video (33.7 vs 30.2 dB) and slightly worse on audio (14.4 vs 15.3 dB SNR).

Tests

  • A standalone harness links the old and new tiling.cpp against the same build and compares their output bitwise over 9 cases, including circular padding and encode: identical.
  • ggml test-backend-ops: CPY passes, and MUL_MAT passes 1186/1186 with the BF16 path forced on.
  • The patch applies cleanly to the pinned ggml submodule commit.

Validation on other hardware

Each arm against master, same seed, Studio's H3 flags (--offload-to-cpu --diffusion-fa --max-vram -1).

hardware result
RTX 6000 Ada (sm89), 960x544x124 Studio default (GGML_CUDA_QUANT_CUBLAS_MIN_BATCH=1024): 152.6 / 149.2 s -> 119.3 / 119.8 s, decode 41.6 / 38.9 -> 22.4 / 22.6 s; max (--sage-attn): 128.5 / 130.1 -> 96.4 / 97.0 s. VAE flash attention alone: 61.3 dB vs master, audio identical. SD_H3_VAE_FLASH_ATTN=0: identical to master.
RTX 3090 (sm86) Studio default 201.6 / 201.1 -> 179.8 / 179.5 s; SD_H3_VAE_FLASH_ATTN=0 identical to master.
Strix Halo gfx1151, ROCm 7.2.1, 640x384x56 CPY and MUL_MAT test-backend-ops pass (also with GGML_CUDA_CPY_ROWS=0 and the BF16 route forced); decode 35.1 -> 22.0 s with VAE flash attention (52.8 dB); =0 identical to master.
Strix Halo, Vulkan (Linux) default identical to master; SD_H3_VAE_FLASH_ATTN=1 58.8 dB.
Strix Halo, Vulkan (Windows, MSVC) builds; CPY passes; default keeps the previous VAE attention.
Apple M1 (Metal) CPY passes; tiled SD1.5 / Z-Image renders identical to master.
  • Tiling rewrite: tiled Z-Image and SD1.5 renders are pixel-identical to master on CUDA, HIP, Vulkan (Linux and Windows) and Metal.
  • BF16 route: against the patch's F32-dequant oracle (GGML_CUDA_QUANT_CUBLAS_TYPE=f32) on a music prompt, it is closer than MMQ on video (43.1 vs 37.1 dB) and audio (28.4 vs 15.3 dB SNR) on the Ada.

Not covered

  • The flash attention kernel used in the denoiser is still about 4x slower than PyTorch's attention on these shapes; this branch does not change it.
  • With the cuBLAS 12.8 that the Linux CUDA prebuilt bundles, the BF16 route is no faster than MMQ on the Ada (82 s either way; the same binary with cuBLAS 12.0 takes 68 s). It is still more accurate.

…rry ggml-cuda speed patch

The H3 video VAE decoder is a 36-layer ViT run per 16x16 latent tile. With only
--diffusion-fa (what Studio passes) it used mul_mat + scale + softmax attention,
materialising a 32 x L x L f32 score matrix per tile and layer. --diffusion-fa now
also covers it; SD_H3_VAE_FLASH_ATTN=0 restores the old path. The text encoder is
unchanged (that is what --fa would also switch, which moves the conditioning).

scripts/unsloth/ggml-patches/ carries two ggml-cuda commits on top of the pinned
submodule (BF16 cuBLAS for large-batch quantized matmul behind
GGML_CUDA_QUANT_CUBLAS_MIN_BATCH, a cached device integrated flag, a vectorized
row copy); the prebuilt workflow applies them after the submodule checkout.
process_tiles_2d copied and blended each pixel across all planes in the inner
loop, striding a whole output plane (960x544 floats for H3) per element. A video
tile has frames x channels planes, so every element was a cache and TLB miss:
on MiniMax-H3 960x544x124 (105 tiles) this was about 10 s of single-threaded
host time per decode. Loop planes outermost and precompute the per-column and
per-row blend weights; each element still computes old + new * wy * wx in the
same order, so the output is bitwise identical.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T03:31:03.300214Z ff941a8 New commits
🔒 Security Review ✅ Completed 2026-10-01T08:30:17.663994Z b01506b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@oobabooga

Copy link
Copy Markdown
Member

Reached Codex review convergence at oobabooga#2.

@oobabooga

Copy link
Copy Markdown
Member

I pushed one change, ff941a8: --diffusion-fa now turns on the video VAE's flash attention only on CUDA and ROCm. On Vulkan the decode was slower with it, and other backends keep the old attention unless SD_H3_VAE_FLASH_ATTN=1. Studio passes --diffusion-fa for every H3 render, so as first written this slowed AMD Vulkan users. The commit was merged down into #18, #19 and #20.

Tested ff941a8 (and ca92d95 before the fix) against master 6321a69, using Studio's H3 flags (--offload-to-cpu --diffusion-fa --max-vram -1). "default" adds GGML_CUDA_QUANT_CUBLAS_MIN_BATCH=1024 as Studio does on sm80+; "max" also adds --sage-attn. Base and head renders are interleaved, 2 of each; times are generate_video in seconds. NVIDIA renders are 960x544x124 at 4 steps; AMD renders are 640x384x56 at 4 steps.

Hardware Mode Render, master → PR VAE decode, master → PR
RTX 6000 Ada (sm89) default 143.7 / 140.3 → 110.5 / 111.0 41.6 / 38.9 → 22.4 / 22.6
max 119.6 / 121.1 → 87.3 / 88.2 39.8 / 43.4 → 24.5 / 24.0
RTX 3090 (sm86) default 192.3 / 192.1 → 170.5 / 170.6 61.3 / 60.3 → 40.6 / 40.5
Strix Halo gfx1151, ROCm 7.2.1 Studio flags 316.3 / 315.9 → 305.2 / 301.3 35.5 / 35.1 → 22.3 / 22.0
Strix Halo, Vulkan (Linux), ca92d95 Studio flags 147.8 / 147.7 → 149.2 / 149.1 54.2 / 54.2 → 55.6 / 55.8
Strix Halo, Vulkan (Linux), ff941a8 Studio flags identical output to master, VAE flash attention off

Output:

  • VAE flash attention changes video by 61.3 dB on the Ada, 52.8 dB on HIP and 58.8 dB on Vulkan (=1). Audio stays bit-identical.
  • SD_H3_VAE_FLASH_ATTN=0 reproduces master bit for bit on the Ada, 3090, HIP and Vulkan, and the switch behaves the same on Windows.
  • The tiling rewrite is bit-identical. Tiled Z-Image and SD1.5 renders match master pixel for pixel on HIP, Vulkan (Linux and Windows) and Metal, as do H3's tiled decodes on CUDA.
  • BF16 cuBLAS route, scored against the patch's F32-dequant oracle on a music prompt (Ada): MMQ scores 37.1 dB video / 15.3 dB audio SNR, and BF16 scores 43.1 / 28.4.

test-backend-ops:

Test Result
CPY, sm89 + sm86 241/241
CPY with GGML_CUDA_CPY_ROWS=0 241/241
MUL_MAT, sm89 + sm86 1186/1186
MUL_MAT with the BF16 route forced 1186/1186
HIP the same four pass
Vulkan CPY (Linux + Windows) 244/244
Metal CPY 191/191
CI job Status
HIP, test-backend-ops pass
HIP, renders pass; master needs a free-memory fix on this APU, so both arms carry the same one-line test patch (fixed for real in #14)
HIP, ff941a8 pass
Vulkan, ca92d95 slower decode, above
Vulkan, ff941a8 pass, identical to master
Windows, MSVC + Vulkan pass

AMD jobs run on DevLab Strix Halo runners (harness). NVIDIA ran locally (RTX 6000 Ada, RTX 3090) and Metal on an M1 Mac mini.

@oobabooga

Copy link
Copy Markdown
Member

Reached Codex review convergence at oobabooga#2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants